package info.motteke.mybws.utils;

import java.beans.PropertyDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * XMLを読み込んでDTOを作成します。
 * 
 * トップレベル要素がroot,
 * 
 * @author YWak
 */
public class BeanBuilder extends DefaultHandler {

    /** ロガー */
    private static final Logger logger = LogManager.getLogger(BeanBuilder.class);

    /** 現在注目しているbeanを積むスタック */
    private final LinkedList<Object> beanStack;

    /** 現在処理している要素に期待されている型のスタック */
    private final LinkedList<String> expectedClassStack;

    /** インターフェース、抽象クラスの実装 */
    private final Map<Class<?>, Class<?>> defaultClasses;

    /** 生成されるクラス */
    private Object root;

    /** 処理中の文字列 */
    private StringBuilder buf;

    /**
     * インスタンスを作成します。
     */
    public BeanBuilder() {
        beanStack = new LinkedList<Object>();
        expectedClassStack = new LinkedList<String>();
        defaultClasses = new HashMap<Class<?>, Class<?>>();
        buf = new StringBuilder();

        registerCollectionsClasses();
    }

    /**
     * Java Collection Frameworkのデフォルト実装クラスを登録します。
     * 
     */
    public void registerCollectionsClasses() {
        defaultClasses.put(Collection.class, ArrayList.class);
        defaultClasses.put(List.class, ArrayList.class);
        defaultClasses.put(Set.class, LinkedHashSet.class);
        defaultClasses.put(Map.class, LinkedHashMap.class);
    }

    /**
     * XMLを読み込んで、インスタンスを返します。
     * 
     * @param input
     *            XMLの読み込み先
     * @return インスタンス
     */
    @SuppressWarnings("unchecked")
    public <T> T parse(InputStream input) {
        Throwable cause;

        try {
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser parser = factory.newSAXParser();

            parser.parse(input, this);

            return (T) root;
        } catch (IOException e) {
            cause = e;
        } catch (SAXException e) {
            cause = e;
        } catch (ParserConfigurationException e) {
            cause = e;
        }

        throw new RuntimeException(cause);
    }

    /*
     * (非 Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#startElement(java.lang.String, java.lang.String, java.lang.String,
     * org.xml.sax.Attributes)
     */
    @Override
    @SuppressWarnings("unchecked")
    public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
        logger.debug("open  : " + qName);

        String className = attributes.getValue("class");

        // rootの場合
        if (qName.equals("root")) {
            createRootElement(attributes);
            return;
        }

        Object current = peek();

        // addの場合はCollectionのことを考える。
        if (qName.equals("add")) {
            if (current instanceof Collection) {
                logger.debug("add collection element");
                // Collectionなら新しい要素を追加する。
                Object next = createCollectionElement(className);

                push(next);
                ((Collection<Object>) current).add(next);
                return;
            } else {
                logger.debug("ignore add for non-collection");
            }
        }

        // プロパティの処理
        Object next = get(current, qName);

        // 次の要素がない場合
        if (next == null) {
            Class<?> clazz = getPropertyType(current, qName);

            if (!representsValue(clazz)) {
                next = create(clazz);
                set(current, qName, next);
            }
        }

        if (next != null && !representsValue(next.getClass())) {
            push(next);
        }

        if (next instanceof Collection) {
            String expectedClassName = attributes.getValue("elementType");
            expectedClassStack.push(expectedClassName);
        }
    }

    // root要素を作成します。
    private void createRootElement(Attributes attributes) {
        String className = attributes.getValue("class");

        if (className == null) {
            throw new RuntimeException("root element requires class attribute.");
        }

        logger.debug("create root element : " + className);

        root = create(searchClass(className));
        push(root);

        return;
    }

    private Object createCollectionElement(String className) {
        logger.debug("create collection element : " + className);

        if (className == null) {
            logger.debug("No class found, search default element type");
            className = expectedClassStack.peek();
        }

        if (className == null) {
            throw new RuntimeException("class can't be determined");
        }

        return create(searchClass(className));
    }

    /*
     * (非 Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#endElement(java.lang.String, java.lang.String, java.lang.String)
     */
    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
        logger.debug("close : " + qName);

        if (qName.equals("root")) {
            return;
        }

        Object current = peek();

        // コレクションの場合は型も取り除く
        if (current instanceof Collection) {
            pop();
            expectedClassStack.pop();
            return;
        }

        if (qName.equals("add")) {
            Object top = pop();

            // addの場合、その前がCollectionなら取り除く
            if (peek() instanceof Collection) {
                return;
            } else {
                push(top);
            }
        }

        if (representsValue(getPropertyType(current, qName))) {
            // 値型ならそれを設定する
            String text = buf.toString().trim();

            if (!text.isEmpty()) {
                set(current, qName, text);
            }

            buf = new StringBuilder();
        } else {
            // 値型でなければトップを取り除く
            pop();
        }

    }

    /*
     * (非 Javadoc)
     * 
     * @see org.xml.sax.helpers.DefaultHandler#characters(char[], int, int)
     */
    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
        buf.append(ch, start, length);
    }

    private Object pop() {
        Object popped = beanStack.pop();

        logger.debug("popped : " + beanStack);

        return popped;
    }

    private void push(Object obj) {
        beanStack.push(obj);

        logger.debug("pushed : " + beanStack);
    }

    private Object peek() {
        return beanStack.peek();
    }

    /**
     * 型が値を表すかどうかを判断します。
     * 
     * @param clazz
     *            型
     * @return 型が値を表すかどうか
     */
    private boolean representsValue(Class<?> clazz) {
        if (clazz.isPrimitive()) {
            logger.debug("Primitives represents value");
            return true;
        }
        if (Number.class.isAssignableFrom(clazz)) {
            logger.debug("Number represents value");
            return true;
        }
        if (clazz.equals(String.class)) {
            logger.debug("String represents value");
            return true;
        }
        if (clazz.equals(Boolean.class)) {
            logger.debug("Boolean represents value");
            return true;
        }

        logger.debug(clazz.getSimpleName() + " does not represent value");
        return false;
    }

    /**
     * プロパティを取得します。
     * 
     * @param bean
     *            プロパティを持つオブジェクト
     * @param name
     *            プロパティ名
     * @return プロパティに設定されている値
     */
    private Object get(Object bean, String name) {
        Throwable cause;
        try {
            logger.debug("get " + name + " from " + bean);
            return PropertyUtils.getProperty(bean, name);
        } catch (IllegalAccessException e) {
            cause = e;
        } catch (NoSuchMethodException e) {
            cause = e;
        } catch (InvocationTargetException e) {
            cause = e.getCause();
        }

        throw new IllegalArgumentException(name, cause);
    }

    /**
     * プロパティに値を設定します。
     * 
     * @param bean
     *            プロパティを持つオブジェクト
     * @param name
     *            プロパティ名
     * @param value
     *            設定する値
     */
    private void set(Object bean, String name, Object value) {
        Throwable cause;
        try {
            logger.debug("set '" + value + "' to " + bean + "#" + name);
            BeanUtils.setProperty(bean, name, value);
            return;
        } catch (IllegalAccessException e) {
            cause = e;
        } catch (InvocationTargetException e) {
            cause = e.getCause();
        }

        throw new IllegalArgumentException(name, cause);
    }

    /**
     * プロパティの型を取得します。
     * 
     * @param bean
     *            プロパティを持つオブジェクト
     * @param name
     *            プロパティ名
     * @return プロパティの型
     */
    private Class<?> getPropertyType(Object bean, String name) {
        Throwable cause;
        try {
            PropertyDescriptor desc = PropertyUtils.getPropertyDescriptor(bean, name);

            return desc.getPropertyType();
        } catch (IllegalAccessException e) {
            cause = e;
        } catch (InvocationTargetException e) {
            cause = e.getCause();
        } catch (NoSuchMethodException e) {
            cause = e;
        }

        throw new RuntimeException(cause);
    }

    /**
     * クラス名から、実際のクラスを取得します。
     * 
     * @param className
     *            クラス名
     * @return クラス
     */
    private Class<?> searchClass(String className) {
        try {
            return Class.forName(className);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * デフォルトコンストラクタを使用してインスタンスを作成します。
     * 実装クラスが登録されている場合はそのクラスを使用します。
     * 
     * @param clazz
     *            インスタンスを生成する型
     * @return インスタンス
     */
    private Object create(Class<?> clazz) {
        Throwable cause;
        try {
            if (defaultClasses.containsKey(clazz)) {
                clazz = defaultClasses.get(clazz);
            }

            return clazz.newInstance();
        } catch (IllegalAccessException e) {
            cause = e;
        } catch (InstantiationException e) {
            cause = e;
        }

        throw new IllegalArgumentException(cause);
    }
}
